Método de Newton para raíces complejas.
Conjuntos de Julia y de Mandelbrot

Javier Pérez González
Departamento de Análisis Matemático
Universidad de Granada

Aproximación de raíces complejas por el método de Newton

Como seguramente ya sabes, el método de Newton es una técnica frecuentemente usada para calcular soluciones de una ecuación f(z) = 0. Para ello, se elige un valor inicial (Subscript[z, 1]) y, a partir de él, se forma la sucesión definida por (Subscript[z, n + 1])=(Subscript[z, n])-((f((Subscript[z, n])))/(f ' ((Subscript[z, n])))). Si dicha sucesión converge, su límite es una solución de la ecuación f(z) = 0. Naturalmente, cuando hay más de una solución, la elección del valor inicial determina a cuál de ellas converge el proceso. 
Mathematica tiene un comando "FindRoot[f(z)=0,{z,a}]" que utiliza precisamente el método de Newton para calcular soluciones de  la ecuación f(z) = 0 a partir del valor inicial z = a. Evalúa la siguiente celda.

FindRoot[(z^2) + 1 == 0, {z, 0.5}]

¿Sabes por qué ha fallado el método en este caso particular? Fíjate en cómo se van obteniendo las aproximaciones sucesivas (Subscript[z, n + 1])=(Subscript[z, n])-((f((Subscript[z, n])))/(f ' ((Subscript[z, n])))). Es evidente que, para f (z)=(z^2)+1 partiendo de un valor real obtenemos siempre valores reales por lo que el método no puede proporcionarnos las raíces de (z^2) + 1  = 0 que son complejas. Repite con una ligera variante.

FindRoot[(z^2) + 1 == 0, {z, 0.5 + 0.1  I}]

Ahora Mathematica ha calculado la solución z = i con gran precisión. Observa ahora el efecto de cambiar un poco el valor inicial.

FindRoot[(z^2) + 1 == 0, {z, 0.5 - 0.1  I}]

Si quieres, puedes ver las sucesivas aproximaciones y los correspondientes valores de la función.

\(z[0] = 0.5 + 0.1  I; 

    z[n_] := \(z[n] = z[n - 1] - \(z[n - 1]\^2 + 1\)\/\(2\ z[n - 1]\)\); 

    Print[TableForm[Table[{k, z[k], z[k]\^2 + 1}, {k, 0, 7}], 
        TableHeadings 
          -> {
            None, {"k", "z[k]", \*""z[k\!\(\(]\^2\)\)+1""}}]]; 
    \)

Como puedes ver Mathematica aplica el método de Newton para calcular raíces tanto reales como complejas. Ahora bien, la intrepretación del método en el caso del cálculo de raíces reales no tiene sentido en el caso complejo. En el caso real, el punto (Subscript[z, n + 1]) se obtiene como el punto de corte de la recta tangente a la curva y = f(z) en el punto ((Subscript[z, n]), f((Subscript[z, n]))) con el eje de abscisas y se conocen condiciones precisas que garantizan la convergencia del método. Lo que ocurre en el caso complejo puede ser mucho más complicado como vamos a ver.

A continuación vamos a aplicar el método de Newton para calcular aproximaciones de las raíces del polinomio (z^3) + 1. Pidámosle a Mathematica que calcule dichas raíces.

raices = 
      z /. (Solve[(z^3) + 1 == 0, 
              z] /. (a_ -> b_) :> (a -> 
                  ComplexExpand[b]))

Sus aproximaciones numéricas son

raices // N

Observa que si f(z) = (z^3) + 1 entonces  z-((f(z))/(f ' (z)))=((-1 + 2 (z^3))/(3 (z^2))).  La siguiente función "NewtonAprox" le dice a Mathematica que, partiendo de un valor inicial  z = x + i y, calcule las sucesivas iteraciones del método de Newton para aproximar una solución de (z^3) + 1 = 0 y que se pare si en algún paso el valor obtenido de z  verifica que |(z^3) + 1|≤0.001 o si ha llegado a la iteración número cincuenta. También le pedimos que imprima el último valor obtenido de z . La función está escrita haciendo uso de la opción "Compile" para que se ejecute más rápidamente.

NewtonAprox = 
        Compile[{{z, _Complex}}, 
          Module[{w, k = 0}, 
            w = z; 
While[
              Abs[(w^3) + 1] > 0.001  &&  k ≤ 50  &&  
                w ≠ 0, w = ((-1 + 2  (w^3))/(3 (w^2))); 
              k = k + 1]; w]];

Comprueba lo rápido que trabaja esta función evaluando la siguiente celda.

Table[
        NewtonAprox[x + y I], {y, -2, 2,  .6}, {x, -2, 2, 1.25}] // 
      MatrixForm

Observa cómo se obtiene una u otra raíz dependiendo del punto de partida. Lo que vamos a hacer ahora es asignarle a cada punto z = x + i y un valor distinto según cuál sea la raíz a la que converja el algoritmo anterior cuando se toma  como valor inicial dicho punto. Concretamente, si la raíz a la que converge el método es -1 le damos a z el valor 0.15, si es 0.5 + 0.866 i le damos el valor 0.5 y si es 0.5 - 0.866 i le damos el valor 0.9. La razón de estos valores es que vamos a usarlos después para aplicarles la función de color "Hue". El color Hue[0.15] es amarillo, el Hue[0.5] es azul y el Hue[0.9] es rojo. Necesitamos redefinir la función "NewtonAprox". Procura entender lo que sigue.

NewtonColorAprox = 
        Compile[{{z, _Complex}}, 
Module[{w, k = 0}, 
            w = z; 
While[
              Abs[(w^3) + 1] > 0.001  &&  k ≤ 50  &&  
                w ≠ 0, w = ((-1 + 2 (w^3))/(3 (w^2))); 
              k = k + 1]; 
Which[
              Abs[w - (-1)] < 0.1, 0.15, 
              Abs[w - (0.5 + 0.866 I)] < 
                0.1, 0.5, 
Abs[
                  w - (0.5 - 0.866 I)] < 
                0.1, 0.9, w == 0, 0.9]] ];

Ahora la función "NewtonColorAprox" ya no proporciona el útlimo valor de z calculado sino el valor asignado a z según el criterio anterior. Como "NewtonColorAprox" da siempre como resultado un número complejo a + b i , e incluso si b = 0 escribe a + 0  i, nos quedamos con la parte real.

Table[
        Re[NewtonColorAprox[x + y I]], {y, -2, 2,  .6}, {x, -2, 2, 
          1.25}] // MatrixForm

Lo que vamos a hacer ahora puede describirse más o menos como sigue. Dividiremos cada lado del cuadrado [-2,2]×[-2,2] en 500 partes, obtendremos así 500×500=250.000 cuadraditos. En cada uno de ellos tomamos un punto (x,y) y evaluamos en dicho punto nuestra función "NewtonColorAprox". Obtenemos así una matriz de 500 filas y 500 columnas a la que aplicamos la función de color "Hue". De esta forma, los puntos que convergen a la raíz -1 se colorean de amarillo, los que convergen a la raíz (1/2) + ((i (Sqrt[3]))/2)se colorean de azul y los que convergen a la raíz (1/2) - ((i (Sqrt[3]))/2)se colorean de rojo. Ten paciencia porque todo el proceso tarda unos 4 minutos pero merece la pena esperar.

DensityPlot[
        NewtonColorAprox[x + y I], {x, -2, 2. },  {y, -2, 2}, 
        PlotPoints -> 500, Mesh -> False, AspectRatio -> 1, 
        ColorFunction -> Hue];

Veamos ampliada la hoja superior del trébol central.

DensityPlot[
        NewtonColorAprox[x + y I], {x, - .6, 0.01},  {y, 0.01,  .8}, 
        PlotPoints -> 200, Mesh -> False, AspectRatio -> 1, 
        ColorFunction -> Hue];

Hagamos ahora una ampliación de la hoja superior del trébol que hay a la derecha.

DensityPlot[
        NewtonColorAprox[x + y I], {x, - .154, -0.088},  {y, 
          0.55,  .7}, PlotPoints -> 200, Mesh -> False, 
        AspectRatio -> 1, ColorFunction -> Hue];

Así podríamos seguir indefinidamente. Observa que, aunque estos puntos están muy próximos convergen a distintas raíces. No parece cosa fácil entender este comportamiento. La frontera del conjunto que estamos representando tiene una estructura fractal. También es una buena imagen de lo que se llama caos matemático porque muestra que pequeñísimos cambios en los valores iniciales conducen a soluciones completamente diferentes.

Los conjuntos de Julia y de Mandelbrot

Los fractales más conocidos son los conjuntos de Julia y de Mandelbrot. Es muy fácil generar dichos conjuntos usando números complejos. Para cada número complejo c definamos la función \(\(f\_c\) : ℂ⟶ℂ\) por (Subscript[f, c])(z) = (z^2) + c para todo z  ∈  ℂ. El conjunto de Julia  asociado al número complejo c se define como el conjunto J(c) de los números complejos z tales que la sucesión definida por (Subscript[z, 0]) = z,  
          (Subscript[z, n]) = (Subscript[z, n - 1]^2) + c = (Subscript[f, c])((Subscript[z, n - 1])) está acotada (algunos autores llaman conjunto de Julia a la frontera de J(c)). El conjunto de Mandelbrot es el conjunto de los números complejos z tales que la sucesión definida por (Subscript[z, 0]) = 0,  
          (Subscript[z, n]) = (Subscript[z, n - 1]^2) + z = (Subscript[f, z])((Subscript[z, n - 1])) está acotada. En otros términos, el conjunto de Mandelbrot es el conjunto de los números complejos z tales que 0 ∈ J(z). 
Para representar la sucesión (Subscript[z, 0]) = z,  
          (Subscript[z, n]) = (Subscript[z, n - 1]^2) + c = (Subscript[f, c])((Subscript[z, n - 1])) suele usarse la notación (Subscript[z, n]) = (Subscript[f, c]^((n)))(z), con el convenio de que (Subscript[z, 0]) = (Subscript[f, c]^((0)))(z) = z. Esta sucesión se llama la órbita de z por (Subscript[f, c]) y sus elementos se obtienen iterando sucesivamente la función (Subscript[f, c]) partiendo del valor inicial z. La notación (Subscript[f, c]^((n)))representa la composición de la función (Subscript[f, c]) consigo misma n veces.

Ejercicio 1 Describe el conjunto de Julia correspondiente a c = 0.

Ejercicio 2 Prueba que si \(\(|\)\(z\)\(|\)\(\(≥\)\(\(|\)\(c\)\(|\)\(\(>\)\(2\)\)\)\)\) entonces  z ∉J(c).

Ejercicio 3 Prueba que si |z|>2 entonces z no está en el conjunto de Mandelbrot. Deduce que si z está en el conjunto de Mandelbrot entonces todos los términos de la sucesión (Subscript[z, 1]) = 0,  (Subscript[z, n + 1]) = (Subscript[z, n]^2) + z están en el disco |z|≤2.

Ejercicio 4 Prueba que el disco |z|≤1/4 está contenido en el conjunto de Mandelbrot.

Ejercicio 5 Si |(Subscript[f, c]^((n)))(
              z)|>max {|c|, 2} para algún n ≥ 0 entonces z  ∉ J(c). En particular, para |c|≤2 el conjunto J(c) está contenido en el disco |z|≤2.

Los conjuntos de Julia y de Mandelbrot se representan con facilidad. El siguiente código procede de la revista  Mathematica Journal.

JuliaFunction = 
        Compile[{{z, _Complex}, {c, _Complex}}, 
          Module[{w, k = 0}, w = z; 
            While[Abs[w] ≤ 2.0  &&  k ≤ 35, 
              w = (w^2) + c; k = k + 1]; k] ];

Este código define una función "JuliaFunction[z,c]" que calcula términos de la órbita de z por (Subscript[f, c]). El proceso se detiene si se encuentra un término de dicha órbita con módulo mayor que 2 y en otro caso continúa hasta calcular el término 35 de la sucesión. Observa que la salida de esta función es el número de iteraciones que realiza más una. Si z ∈  J(c) entonces -JuliaFunction[z,c]=36, y si -JuliaFunction[z,c]<36 se tiene que z  ∉ J(c). Es posible que para algunos valores "raros" de z sean necesarias más de 35 iteraciones para saber si están o no en el conjunto de Julia, para tales valores podría ocurrir que Function[z,c]=36 pero z  ∉ J(c). El conjunto de estos valores "raros" es tan pequeño que no afecta realmente al aspecto final del conjunto.

Calculemos los valores de "JuliaFunction[x+yI,-1]" en cada uno de los puntos (x,y) del retículo siguiente.

ListPlot[
        Flatten[Table[{x, y}, {y, -1.1, 1.1,  .1}, {x, -1.5, 
              1.5,  .1}], 1]];

datos = 
      Table[JuliaFunction[x + y I, -1], {y, -1.1, 
          1.1,  .1}, {x, -1.5, 1.5,  .1}]

Podemos asignar un color a cada punto (x,y) según cuál sea el valor en dicho punto de la función JuliaFunction[x+yI,-1]. Es importante que entiendas cómo Mathematica asigna colores. Recuerda que la función "Hue" es periódica con período 1, es decir, Hue[x]=Hue[x+1]. Aquí puedes ver cómo asigna colores dicha función.

Show[
        Graphics[({Hue[#], Rectangle[{#, 0}, {#1 +  .1, 1}]} &) /@ 
            Range[0, 2.5,  .1], AspectRatio -> Automatic]];

Usemos "ListDensityPlot" para representar la tabla de valores, que hemos llamado "datos", antes obtenida. Como el valor de JuliaFunction[x+yI,-1] es siempre un número natural, se tiene que Hue[JuliaFunction[x+yI,-1]]=Hue[0]. Para evitar asignar a todos los puntos el mismo color,  Mathematica hace una reducción a escala de los valores de la función JuliaFunction[x+yI,-1] antes de aplicar la función "Hue". Concretamente, en el caso que nos ocupa, el color que Mathematica asigna al punto (x,y) es  Hue[JuliaFunction[x+yI,-1]/36]. Por ello los puntos en rojo están en el conjunto de Julia aunque también puntos alejados de dicho conjunto, por ejemplo aquellos en los que JuliaFunction[x+yI,-1]=1, se representan en un color rojizo pues Hue[1/36] es parecido a Hue[0]=Hue[1].

ListDensityPlot[datos, ColorFunction -> Hue];

Observa la diferencia entre la gráfica antes obtenida y la que sigue en la que hemos modificado la función de color de forma que distinga mejor los puntos del conjunto de Julia.

ListDensityPlot[datos, 
        ColorFunction -> (Hue[
                If[# ≥ 7, #/36, #/12]] &), 
        ColorFunctionScaling -> False];

Hagamos lo mismo pero con más puntos. Es cómodo definir una función "datosjulia" como sigue.

\(Clear[datos]; 

    datosjulia[c_, inc_] := 
      \(datosjulia[c, inc] = 
        Table[JuliaFunction[x + y\ I, c], {y, \(-1.1\), 1.1, inc}, {x, 
            \(-1.5\), 1.5, inc}]\)\)

Ahora representamos datos[-1,0.005] (si tu ordenador es lento, o eres muy impaciente, sustituye el incremento 0.005 por 0.01).

ListDensityPlot[datosjulia[-1, 0.005], Frame -> False, 
        Mesh -> False, ColorFunction -> Hue, 
        AspectRatio -> 3/4, 
        PlotLabel -> 
          StyleForm["El conjunto de Julia generado por c=-1", 
            FontFamily -> Times, FontSize -> 14], 
        ImageSize -> {374, 250}];

Exactamente lo mismo puede conseguirse directamente con el comando "DensityPlot".

DensityPlot[
        JuliaFunction[x + y I, -1], {x, -1.5, 1.5}, {y, -1.1, 
          1.1}, PlotPoints -> 500, ColorFunction -> Hue, 
        Mesh -> False, AspectRatio -> Automatic, 
        Frame -> False, 
        PlotLabel -> 
          StyleForm["El conjunto de Julia generado por c=-1", 
            FontFamily -> Times, FontSize -> 14], 
        ImageSize -> {374, 250}];

La ventaja de proceder como antes es que Mathematica recuerda la lista "datosjulia[-1,0.005]" y, si queremos, podemos volver a representarla cambiando, por ejemplo, la función de color sin necesidad de volver a obtener los datos. Compruébalo.

ListDensityPlot[datosjulia[-1, 0.005], Frame -> False, 
        Mesh -> False, 
        ColorFunction -> (Hue[
                If[# ≤  5, 1/(2  #), 1/#]] &), 
        AspectRatio -> 1/GoldenRatio, 
        PlotLabel -> 
          StyleForm["El conjunto de Julia generado por c=-1", 
            FontFamily -> Times, FontSize -> 14], 
        ImageSize -> {374, 250}];

Te sugiero que representes los conjuntos de Julia para valores de c igual a -0.5, 0.360284 + 0.100376 i, -0.122 + 0.745 i .

ListDensityPlot[datosjulia[0.360284  +  0.100376 I, 0.005], 
        Frame -> False, Mesh -> False, 
        ColorFunction -> (Hue[If[# == 36, #, 1/(# +  .1)]] &), 
        AspectRatio -> 2/3, 
        PlotLabel -> 
          StyleForm["El conjunto de Julia generado por c=0.3060284+0.100376 \
\[ImaginaryI]", FontFamily -> Times, FontSize -> 14], 
        ImageSize -> {374, 250}];

El siguiente código genera el conjunto de Mandelbrot.

MandelbrotFunction = 
        Compile[{{z, _Complex}}, 
          Module[{w, k = 0}, w = z; 
            While[Abs[w] ≤ 2.0  &&  k ≤ 50, 
              w = (w^2) + z; k = k + 1]; k] ];

Te recuerdo otra vez que si tu ordenador es lento, o eres muy impaciente, sustituye el incremento 0.005 por 0.01  antes de evaluar la siguiente celda.

datosmandelbrot = 
        Table[MandelbrotFunction[x  +  y I],  {y, -1.15, 
            1.15,  .005}, {x, -2.0, 0.55,  .005}];

ListDensityPlot[datosmandelbrot, Mesh -> False, 
        Frame -> False, ColorFunction -> Hue, 
        PlotLabel -> 
          StyleForm["El conjunto de Mandelbrot", 
            FontFamily -> Times, FontSize -> 14], 
        ImageSize -> {341, 341}];

Fractales maravillosos

Cambiemos la función de color para ver su efecto en el conjunto de Mandelbrot.

ListDensityPlot[datosmandelbrot, Mesh -> False, 
        Frame -> False, 
        ColorFunction -> (Hue[If[# == 36, #, 1/(# +  .1)]] &), 
        PlotLabel -> 
          StyleForm["El conjunto de Mandelbrot", 
            FontFamily -> Times, FontSize -> 14], 
        ImageSize -> {341, 341}];

Con una pequeña modificación en los códigos anteriores puedes conseguir gráficos sorprendentes. La modificación consiste en pedirle a la función que nos devuelva el módulo del valor de la última iteración.

JuliaFunctionNew = 
        Compile[{{z, _Complex}, {c, _Complex}}, 
          Module[{w, k = 0}, w = z; 
            While[Abs[w] ≤ 2.0  &&  k ≤ 35, 
              w = (w^2) + c; k = k + 1]; Abs[w]] ];

datosjulianew[c_, inc_] := datosjulianew[c, inc] = 
        Table[JuliaFunctionNew[x + y I, c], {y, -1.5, 1.5, 
            inc}, {x, -1.3, 1.3, inc}]

ListDensityPlot[datosjulianew[0.360284  +  0.100376 I, 0.005], 
        Frame -> False, Mesh -> False, ColorFunction -> Hue, 
        AspectRatio -> (2/3), 
        PlotLabel -> 
          StyleForm["El conjunto de Julia generado por c=0.360284+0.100376\
\[ImaginaryI]", FontFamily -> Times, FontSize -> 14], 
        ImageSize -> {374, 250}];

MandelbrotFunctionNew = 
        Compile[{{z, _Complex}}, 
          Module[{w, k = 0}, w = z; 
            While[Abs[w] ≤ 2.0  &&  k ≤ 50, 
              w = (w^2) + z; k = k + 1]; Abs[w]] ];

datosmandelbrotnew = 
        Table[MandelbrotFunctionNew[x  +  y I], {y, -1.15, 1.15, 
            0.005}, {x, -2.0, 0.55, 0.005}];

ListDensityPlot[datosmandelbrotnew, Mesh -> False, 
        Frame -> False, ColorFunction -> Hue, 
        PlotLabel -> 
          StyleForm["El conjunto de Mandelbrot", 
            FontFamily -> Times, FontSize -> 14], 
        ImageSize -> {341, 341}];

Observa el efecto de un cambio en la función de color.

ListDensityPlot[datosmandelbrotnew, Mesh -> False, 
        Frame -> False, ColorFunction -> (Hue[Exp[-#]] &), 
        PlotLabel -> 
          StyleForm["El conjunto de Mandelbrot", 
            FontFamily -> Times, FontSize -> 14], 
        ImageSize -> {341, 341}];

Para terminar, volvamos al comienzo pero sustituyendo el polinomio (z^3) + 1 por (z^5) + 1 y cambiando un poco el código.

NewtonAproxNew = 
        Compile[{{z, _Complex}}, 
Module[{w, k = 0}, 
            w = z; 
While[
              Abs[(w^5) + 1] > 0.001  &&  k ≤ 50  &&  
                w ≠ 0, w = ((-1 + 4  (w^5))/(5 (w^4))); 
              k = k + 1]; k]];

datospolinomionew = 
        Table[NewtonAproxNew[x  +  y I], {y, -2, 
            2,  .00345}, {x, -2, 2,  .00877}];

ListDensityPlot[datospolinomionew, Mesh -> False, 
        Frame -> False, ColorFunction -> (Hue[1 - #] &), 
        ImageSize -> {383, 383}];

¡Qué maravilla! ¿Quién ha dicho que las matemáticas no son hermosas?